﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Diagnostics;
using Roslyn.Compilers.CSharp;
using System.Threading;
using System.Reflection;
using Util;
using CompleterLib.Util;

namespace CompleterLib.AbstractTypes
{
    class TypeTermParser
    {
        private readonly SemanticModel semModel;
        private readonly int position;
        public TypeTermParser(SemanticModel semModel, int position)
        {
            this.semModel = semModel;
            this.position = position;
        }

        public IEnumerable<TypeConstraint> GetTypeConstraints(StreamReader stream)
        {
            foreach (var ttset in ParseTypeMeanings(stream))
            {
                var firstTT = ttset.FirstOrDefault();
                if (firstTT != null)
                {
                    foreach (var tt in ttset.Skip(1))
                    {
                        yield return new TypeConstraint(firstTT, tt);
                    }
                }
            }
        }

        public IEnumerable<IEnumerable<TypeTerm>> ParseTypeMeanings(StreamReader stream)
        {
            if (!stream.ReadLine().StartsWith("-----"))
            {
                //throw new Exception("Not a type meanings file.");
                yield break;
            }
            else
            {
                while (!stream.EndOfStream)
                {
                    yield return ParseTypeMeaning(stream);
                }
            }
        }
        public IEnumerable<TypeTerm> ParseTypeMeaning(StreamReader stream)
        {
            string line;
            while (!stream.EndOfStream && !(line = stream.ReadLine()).Equals(""))
            {
                TypeTerm tt = ParseTypeTerm(line.Substring(line.IndexOf(' ') + 1));
                if (tt != null)
                {
                    yield return tt;
                }
            }
        }
        public NamespaceOrTypeSymbol ParseNamespaceOrTypeSymbol(string str)
        {
            int dotIdx = str.LastIndexOf('.');
            NamespaceOrTypeSymbol within;
            string name;
            if (dotIdx == -1)
            {
                within = null;
                name = str;
                // TODO Generic types? Array types?
                return semModel.LookupSymbols(position, within, name)
                    .FirstOrDefault(sym => sym is NamespaceOrTypeSymbol) as NamespaceOrTypeSymbol;
            }
            else
            {
                within = ParseNamespaceOrTypeSymbol(str.Substring(0, dotIdx));
                if (within == null)
                {
                    // This seems to only happen with interface names in the method name.
                    //  (really a bug on the other side...)
                    return null;
                }
                name = str.Substring(dotIdx + 1);
                return within.GetMembers(name).AsEnumerable()
                    .FirstOrDefault(sym => sym is NamespaceOrTypeSymbol) as NamespaceOrTypeSymbol;
            }
        }
        public TypeSymbol ParseTypeSymbol(string str)
        {
            return ParseNamespaceOrTypeSymbol(str) as TypeSymbol;
        }
        private static IEnumerable<string> WithoutUnbalancedAngleBrackets(IEnumerable<string> xs)
        {
            string saved = null;
            foreach (var x in xs)
            {
                string currentStr = saved == null ? x : saved + ", " + x;
                int balance = 0;
                foreach (char c in currentStr)
                {
                    if (c == '<') balance++;
                    else if (c == '>') balance--;
                }
                if (balance == 0)
                {
                    saved = null;
                    yield return currentStr;
                }
                else
                {
                    saved = currentStr;
                }
            }
            if (saved != null)
            {
                throw new Exception("WithoutUnbalancedAngleBrackets failed.");
            }
        }
        private Symbol ParseMethodSymbol(string func, string funcType)
        {
            int dotIdx = func.LastIndexOf('.');
            IList<MethodSymbol> symbols;
            bool isConstructor;
            if (func.EndsWith(".ctor"))
            {
                // TODO Static constructors?
                isConstructor = true;
                TypeSymbol containingType = ParseTypeSymbol(func.Substring(0, dotIdx - 1));
                if (containingType == null)
                {
                    return null;
                }
                symbols = (containingType as NamedTypeSymbol).InstanceConstructors
                    .AsList();
            }
            else
            {
                isConstructor = false;
                TypeSymbol containingType = ParseTypeSymbol(func.Substring(0, dotIdx));
                string methodName = func.Substring(dotIdx + 1);
                if (methodName.StartsWith("get_") || methodName.StartsWith("set_"))
                {
                    string propName = methodName.Substring(4);
                    var props = semModel.LookupSymbols(position, containingType, propName)
                        .Where(sym => sym is PropertySymbol).ToList();
                    if (props.Count == 1)
                    {
                        return props[0];
                    }
                    else if (props.Count == 0)
                    {
                        return null;
                    }
                    else
                    {
                        return null;
                        //throw new Exception("Multiple properties match.");
                    }
                }
                else
                {
                    symbols = semModel.LookupSymbols(position, containingType, methodName)
                        .Where(sym => sym is MethodSymbol).Cast<MethodSymbol>().ToList();
                }
            }
            if (symbols.Count == 0)
            {
                //throw new Exception("Unknown function: " + info);
                return null;
            }
            else if (symbols.Count == 1)
            {
                return symbols[0];
            }
            else
            {
                string[] funcTypeSplit = funcType.Split(" => ".Singleton(),
                                                        StringSplitOptions.None);
                string argsStr = funcTypeSplit[0];

                if (!isConstructor)
                {
                    string retTypeStr = funcTypeSplit[1];
                    TypeSymbol retType = ParseTypeSymbol(retTypeStr);
                    symbols = symbols.Where(sym => sym.ReturnType.Equals(retType)).ToList();
                    if (symbols.Count == 1)
                    {
                        return symbols[0];
                    }
                }

                // Remove surrounding parens.
                argsStr = argsStr.Substring(1, argsStr.Length - 2);
                List<TypeSymbol> paramTypes = WithoutUnbalancedAngleBrackets(
                        argsStr.Split(", ".Singleton(), StringSplitOptions.None))
                    .Select(s => ParseTypeSymbol(s)).ToList();
                symbols = symbols
                    .Where(sym => paramTypes.Count
                        == (sym.Parameters.Count + (sym.IsStatic ? 0 : 1)))
                    .ToList();
                if (symbols.Count == 1)
                {
                    return symbols[0];
                }

                symbols = symbols
                    .Where(sym => paramTypes.Skip(sym.IsStatic ? 0 : 1)
                        .SequenceEqual(sym.Parameters.AsEnumerable().Select(p => p.Type)))
                    .ToList();
                if (symbols.Count == 1)
                {
                    return symbols[0];
                }
                else if (symbols.Count == 0)
                {
                    // Not found...
                    return null;
                }
                else
                {
                    // Happens for Object.Equals() at least...
                    //throw new Exception("Non-exact match: " + symbols);
                    return null;
                }
            }
        }
        public TypeTerm ParseTypeTerm(string str)
        {
            int firstParens = str.IndexOf('(');
            string type = str.Substring(0, firstParens);
            string info = str.Substring(firstParens + 1, str.LastIndexOf(')') - firstParens - 1);

            //Logging.Logger.PrintLine("DEBUG: ParseTypeTerm: str=" + str);
            //Logging.Logger.PrintLine("DEBUG: ParseTypeTerm: type=" + type + ", info=" + info);

            // TODO
            if (type.Equals("FormalParameterTypeTerm"))
            {
                string[] infos = info.Split(new string[] { "func=", ", funcType=", ", position=" },
                                            StringSplitOptions.None);
                string func = infos[1];
                string funcType = infos[2];
                string position = infos[3];
                int pos = int.Parse(position);
                Symbol symbol = ParseMethodSymbol(func, funcType);

                if (symbol == null)
                {
                    return null;
                }
                else if(symbol is MethodSymbol)
                {
                    return new FormalParameterTypeTerm(symbol as MethodSymbol, pos);
                }
                else if (symbol is PropertySymbol)
                {
                    if (pos == 0)
                    {
                        return null;
                    }
                    else if (symbol.Name.Equals("Item"))
                    {
                        return null;
                    }
                    else // pos == 1
                    {
                        return new NamedIdentifiedTypeTerm(symbol as PropertySymbol);
                    }
                }
                else
                {
                    throw new Exception();
                }
            }
            else if (type.Equals("ReturnTypeTypeTerm"))
            {
                string[] infos = info.Split(new string[] { "func=", ";type=" },
                            StringSplitOptions.None);
                string func = infos[1];
                string funcType = infos[2];
                Symbol symbol = ParseMethodSymbol(func, funcType);

                if (symbol == null)
                {
                    return null;
                }
                else if(symbol is MethodSymbol)
                {
                    return new ReturnTypeTypeTerm(symbol as MethodSymbol);
                }
                else if (symbol is PropertySymbol)
                {
                    return new NamedIdentifiedTypeTerm(symbol as PropertySymbol);
                }
                else
                {
                    throw new Exception();
                }
            }
            else if (type.Equals("NamedTypeTerm"))
            {
                return new NamedTypeTerm(info);
            }
            else if (type.Equals("NamedIdentifiedTypeTerm"))
            {
                return new NamedIdentifiedTypeTerm(info);
            }
            else if (type.Equals("ConstantTypeTerm"))
            {
                return new ConstantTypeTerm(info);
            }
            else if (type.Equals("ConstantStringTypeTerm"))
            {
                // Remove double quotes from string.
                return new ConstantStringTypeTerm(info.Substring(1, info.Length - 1));
            }
            else
            {
                throw new Exception("Unknown TypeTerm type: " + type);
            }
        }

        public IEnumerable<TypeConstraint> GetExternalTypeConstraints(
                IList<Roslyn.Compilers.MetadataReference> references,
                CancellationToken token)
        {
            return (from reference in references
                    select GetExternalTypeConstraints(reference, token))
                   .Concat();
        }

        private static Dictionary<Roslyn.Compilers.MetadataReference, IList<TypeConstraint>>
            typeConstraints = new Dictionary<Roslyn.Compilers.MetadataReference, IList<TypeConstraint>>();
        private IList<TypeConstraint> GetExternalTypeConstraints(
                Roslyn.Compilers.MetadataReference reference,
                CancellationToken token)
        {
            IList<TypeConstraint> res;
            if (!typeConstraints.TryGetValue(reference, out res))
            {
                res = _GetExternalTypeConstraints(reference, token).ToList();
            }
            return res;
        }

        private IEnumerable<TypeConstraint> _GetExternalTypeConstraints(
                Roslyn.Compilers.MetadataReference reference,
                CancellationToken token)
        {
            if (reference is Roslyn.Compilers.AssemblyFileReference)
            {
                return GetExternalTypeConstraints(
                        (reference as Roslyn.Compilers.AssemblyFileReference).Path,
                        token);
            }
            else if (reference is Roslyn.Compilers.CSharp.CompilationReference)
            {
                var compRef = reference as Roslyn.Compilers.CSharp.CompilationReference;
                var comp = compRef.Compilation;
                var cc = ConstraintCollector.GetForCompilation(comp);
                return cc.Constraints;
            }
            else
            {
                throw new NotImplementedException();
            }
        }


        public static string ExtensionDirectory
        {
            set
            {
                var di = new DirectoryInfo(value);
                LOG_DIR = di.CreateSubdirectory("abstypes").FullName;
                EXTERNAL_ABSTYPES_PROG = di.GetFiles("ExternalAbstractTypeCollector.exe")[0].FullName;
            }
        }
        private static DirectoryInfo DotNotImplementationDir
        {
            get
            {
                if (DOT_NET_IMPLEMENTATION_DIR == null)
                {
                    var winDir = new DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.Windows));
                    DOT_NET_IMPLEMENTATION_DIR = winDir.GetDirectories("Microsoft.NET")[0];
                }
                return DOT_NET_IMPLEMENTATION_DIR;
            }
        }

        private static string LOG_DIR = @"s:\log";
        private static string EXTERNAL_ABSTYPES_PROG = typeof(ExternalAbstractTypeCollector.Program).Assembly.Location;
        //const string DOT_NET_REFERENCE_DIR = @"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\Profile\Client";
        private static DirectoryInfo DOT_NET_IMPLEMENTATION_DIR = null;// @"%WINDOWS%\Microsoft.NET\Framework\v4.0.30319\";
        private IEnumerable<TypeConstraint> GetExternalTypeConstraints(
                string assemblyPath,
                CancellationToken token)
        {
            token.ThrowIfCancellationRequested();

            var assemblyName = AssemblyName.GetAssemblyName(assemblyPath);
            FileInfo assemblyFileInfo = new FileInfo(assemblyPath);
            DateTime assemblyLastWrite = assemblyFileInfo.LastWriteTime;

            var filename = assemblyFileInfo.Name;
            // Special case because mscorlib.dll seems to cause problems.
            if (filename.Equals("mscorlib.dll"))
            {
                return Enumerable.Empty<TypeConstraint>();
            }
            string log = Directory.EnumerateFiles(LOG_DIR,
                    filename
                    + "-" + assemblyName.Version
                    + "-" + assemblyName.GetPublicKeyToken().ToHexString()
                    + ".abstypes.*.txt")
                .ArgMaxOrDefault(path => File.GetLastWriteTime(path));
            StreamReader stream;
            if (log != null
                && new FileInfo(log).Length > 0
                && File.GetLastWriteTime(log) > assemblyLastWrite)
            {
                stream = new StreamReader(log);
                return GetTypeConstraints(stream);
            }
            else
            {
                if (assemblyFileInfo.DirectoryName.Contains("Reference Assemblies")
                    && assemblyFileInfo.DirectoryName.Contains(".NET"))
                {
                    var refAssemblyName = assemblyName;
                    assemblyPath = DotNotImplementationDir.EnumerateFiles(filename, SearchOption.AllDirectories)
                        .First(f => AssemblyName.GetAssemblyName(f.FullName).FullName
                                    .Equals(refAssemblyName.FullName))
                        .FullName;
                }

                Process p = new Process();
                // Only do processing in the background.
                p.PriorityClass = ProcessPriorityClass.Idle;
                p.StartInfo.FileName = EXTERNAL_ABSTYPES_PROG;
                p.StartInfo.Arguments = "\"" + assemblyPath + "\" --abstypes \"" + LOG_DIR + "\"";
                p.StartInfo.UseShellExecute = false;
                p.StartInfo.CreateNoWindow = true;
                //p.StartInfo.RedirectStandardOutput = true;
                p.Start();
                // Check every 100ms (tenth of a second) for cancellation.
                while (!p.WaitForExit(100))
                {
                    if (token.IsCancellationRequested)
                    {
                        p.CloseMainWindow();
                        p.Kill();
                        p.Close();
                        token.ThrowIfCancellationRequested();
                    }
                }
                return GetExternalTypeConstraints(assemblyPath, token);

                //stream = p.StandardOutput;
            }
        }
    }
}
